﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Magellan.Diagnostics;

namespace Magellan.Framework
{
    /// <summary>
    /// A base class for view engines that use reflection to locate the view based on namespace and type 
    /// name conventions.
    /// </summary>
    public abstract class ReflectionBasedViewEngine : IViewEngine
    {
        private readonly List<Assembly> _additionalViewAssemblies = new List<Assembly>();

        /// <summary>
        /// Initializes a new instance of the <see cref="ReflectionBasedViewEngine"/> class.
        /// </summary>
        protected ReflectionBasedViewEngine(params Assembly[] additionalViewAssemblies)
        {
            NamespaceConvention = new DefaultViewNamespaceConvention();
            NamingConvention = new DefaultViewNamingConvention();

            if (additionalViewAssemblies == null) return;
            foreach (var assembly in additionalViewAssemblies)
            {
                AdditionalViewAssemblies.Add(assembly);
            }
        }

        /// <summary>
        /// Gets the a collection of additional assemblies that will be used to discover a view. 
        /// </summary>
        /// <value>The additional view assemblies.</value>
        public IList<Assembly> AdditionalViewAssemblies
        {
            get { return _additionalViewAssemblies; }
        }

        /// <summary>
        /// Gets or sets the view namespace provider. This class is used to generate the combinations of view 
        /// locations and to encapsulate conventions surrounding the project structure.
        /// </summary>
        /// <value>The view namespace provider.</value>
        public IViewNamespaceConvention NamespaceConvention { get; set; }

        /// <summary>
        /// Gets or sets the view namespace provider. This class is used to generate the combinations of view 
        /// locations and to encapsulate conventions surrounding the project structure.
        /// </summary>
        /// <value>The view namespace provider.</value>
        public IViewNamingConvention NamingConvention { get; set; }

        /// <summary>
        /// When implemented in a derived class, indicated whether this view engine should even attempt to 
        /// locate views and handle the request. If this method returns false, no reflection will be 
        /// performed.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="options">The view parameters.</param>
        /// <param name="viewName">Name of the view.</param>
        /// <returns></returns>
        protected virtual bool ShouldHandle(ControllerContext controllerContext, ViewResultOptions options, string viewName)
        {
            return true;
        }

        /// <summary>
        /// When implemented in a derived class, allows the derived class to restrict the criteria used to select potential types.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="options">The view parameters.</param>
        /// <param name="viewName">Name of the view.</param>
        /// <param name="candidates">The list of candidate types.</param>
        /// <returns></returns>
        protected abstract IEnumerable<Type> FilterCandidateTypes(ControllerContext controllerContext, ViewResultOptions options, string viewName, IEnumerable<Type> candidates);

        /// <summary>
        /// Creates the view result for the specified view type.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="options">The view parameters.</param>
        /// <param name="type">The type.</param>
        /// <returns></returns>
        protected abstract ViewEngineResult CreateViewResult(ControllerContext controllerContext, ViewResultOptions options, Type type);

        /// <summary>
        /// Attempts to find the view, or returns information about the locations that were searched.
        /// </summary>
        /// <param name="controllerContext">The controller context.</param>
        /// <param name="options">The view parameters.</param>
        /// <param name="view">The view.</param>
        /// <returns>
        /// A <see cref="ViewEngineResult"/> containing the resolved view or information about the locations that were searched.
        /// </returns>
        public ViewEngineResult FindView(ControllerContext controllerContext, ViewResultOptions options, string view)
        {
            if (!ShouldHandle(controllerContext, options, view))
            {
                return new ViewEngineResult(false, new string[0]);
            }

            var searchAttempts = new List<string>();
            var assembly = controllerContext.Controller.GetType().Assembly;
            var allTypes = assembly.GetTypes().Union(
                _additionalViewAssemblies.SelectMany(x => x.GetTypes())
                );

            var types = new Type[0].AsEnumerable();
            var alternativeNames = NamingConvention.GetAlternativeNames(controllerContext, view);
            foreach (var alternativeName in alternativeNames)
            {
                var localAlternativeName = alternativeName;
                types = types.Union(allTypes.Where(x => string.Equals(x.Name, localAlternativeName, StringComparison.InvariantCultureIgnoreCase)));
            }

            types = FilterCandidateTypes(controllerContext, options, view, types);

            // First we create a list of namespaces - for example:
            // - MyProject.Views.Home
            // - MyProject.Views
            // - MyProject
            // - (empty)
            var namespaces = new List<string>();
            var currentNamespace = controllerContext.Controller.GetType().Namespace;
            while (currentNamespace != null && currentNamespace.Trim().Length > 0)
            {
                namespaces.AddRange(NamespaceConvention.GetNamespaces(controllerContext, currentNamespace, view));

                var lastIndex = currentNamespace.LastIndexOf('.');
                if (lastIndex < 0) break;
                currentNamespace = currentNamespace.Substring(0, lastIndex);
            }
            namespaces.Add(null);

            // Now for each namespace, we want to find a view by applying the suffixes. In order we end up 
            // trying:
            // - MyProject.Views.Home.Home 
            // - MyProject.Views.Home.HomeView
            // - MyProject.Views.Home.HomeWindow
            // - MyProject.Views.Home
            // - MyProject.Views.HomeView
            // - MyProject.Views.HomeWindow
            // - MyProject.Home
            // - MyProject.HomeView
            // - MyProject.HomeWindow
            // - Home
            // - HomeView
            // - HomeWindow
            foreach (var namespaceName in namespaces)
            {
                var type = types.FirstOrDefault(x => string.Equals(x.Namespace, namespaceName, StringComparison.InvariantCultureIgnoreCase));
                if (type != null)
                {
                    var result = CreateViewResult(controllerContext, options, type);
                    if (result.Success)
                    {
                        return result;
                    }
                }

                foreach (var alternativeName in alternativeNames)
                {
                    searchAttempts.Add(string.IsNullOrEmpty(namespaceName)
                                           ? string.Format("{0}", alternativeName)
                                           : string.Format("{0}.{1}", namespaceName, alternativeName));
                }
            }

            TraceSources.MagellanSource.TraceInformation("The {0} could not find the view '{1}'. The following locations were searched: \r\n{2}",
                GetType().FullName,
                view,
                string.Join("\r\n- ", searchAttempts.ToArray())
                );

            return new ViewEngineResult(false, searchAttempts);
        }
    }
}
